package com.iddactylogram.client;

import com.iddactylogram.common.core.SeqIdProvider;
import com.iddactylogram.common.domain.MessageObject;
import com.iddactylogram.common.domain.NetworkAddress;
import com.iddactylogram.common.enumerate.CmdEnums;
import com.iddactylogram.common.utils.MessageParseUtils;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class IdDactylogramClient {

    private final static Logger LOG = Logger.getLogger(IdDactylogramClient.class.getName());
    private int maxRetryTimes = 10;
    private int sleepMsRetryBetween = 100;

    private final AtomicInteger retryTimes = new AtomicInteger(0);
    private final static ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(1);
    private NetworkAddress currServerNetworkAddress = null;

    private SocketChannel socketChannel;
    private final Selector selectorRead;
    private final String applicationName;
    private final ILoadBalancing loadBalancing;

    public IdDactylogramClient(ILoadBalancing loadBalancing, String applicationName) throws IOException {
        this.loadBalancing = loadBalancing;
        this.applicationName = applicationName;
        selectorRead = Selector.open();
        scheduledPool.scheduleAtFixedRate(this::heartCheck, 30, 30, TimeUnit.SECONDS);
        init();
    }

    private void init() throws IOException {
        loadBalancing.loadServerInfo(this::runReloadAfter);
        NetworkAddress networkAddress = loadBalancing.chooseServerAddress(applicationName);
        this.currServerNetworkAddress = networkAddress;
        socketChannel = SocketChannel.open(new InetSocketAddress(networkAddress.getIp(), networkAddress.getPort()));
        socketChannel.configureBlocking(false);
        socketChannel.register(selectorRead, SelectionKey.OP_READ);
    }

    public long getNextId() {
        try {
            MessageObject messageObject = sendData(applicationName.getBytes(StandardCharsets.UTF_8), CmdEnums.ID_GEN);
            return Long.parseLong(messageObject.getMessage());
        } catch (Exception e) {
            long id = SeqIdProvider.genId(applicationName);
            System.err.println("使用兜底方案：在本地产生一个分布式ID:" + id);
            return id;
            //TODO 最后的兜底方案，直接使用本地的生成，这里需要发出报警
        }
    }

    public void heartCheck() {
        try {
            loadBalancing.loadServerInfo(this::runReloadAfter);
            NetworkAddress networkAddress = loadBalancing.chooseServerAddress(this.applicationName);
            if (!networkAddress.equals(currServerNetworkAddress)){
                socketChannel = SocketChannel.open(new InetSocketAddress(networkAddress.getIp(), networkAddress.getPort()));
                socketChannel.configureBlocking(false);
                socketChannel.register(selectorRead, SelectionKey.OP_READ);
                System.out.println("进行重连。。。。。。。。。");
            }else {
                System.out.println("不需要重新建立新的链接");
            }

            MessageObject messageObject = sendData(applicationName.getBytes(StandardCharsets.UTF_8), CmdEnums.HEART);
            String message = messageObject.getMessage();
            System.out.println("心跳检查：" + message);
        } catch (Exception e) {
            throw new IllegalStateException("心跳检测出现异常:", e);
        }
    }

    private synchronized MessageObject sendData(byte[] data, CmdEnums cmd) {
        try {
            ByteBuffer writeBuf = MessageParseUtils.createWriteBuf(data, cmd.getCode());
            writeBuf.flip();
            socketChannel.write(writeBuf);
            return readData();
        } catch (ReadFailException e) {
            return tryResend(data, cmd);
        } catch (Exception e) {
            LOG.log(Level.INFO, "sendData()执行失败：", e);
            throw new IllegalStateException(e);
        }
    }

    //失败的情况下尝试重新发送
    private MessageObject tryResend(byte[] data, CmdEnums cmd) {
        try {
            System.out.println("开始重试======");
            //从新加载数据
            if (socketChannel != null && socketChannel.isOpen()) {
                socketChannel.close();
            }
            TimeUnit.MILLISECONDS.sleep(sleepMsRetryBetween);
            init();
            long times = retryTimes.incrementAndGet();
            if (times > maxRetryTimes) {
                throw new IllegalStateException("tryResend faile cause more max retry times");
            } else {
                return sendData(data, cmd);
            }
        } catch (Exception e) {
            throw new IllegalStateException("tryResend faile", e);
        }
    }

    private MessageObject readData() throws IOException, ReadFailException {
        ByteBuffer buffer = ByteBuffer.allocate(128);
        AtomicInteger readLen = new AtomicInteger();

        selectorRead.select();
        Iterator<SelectionKey> iterator = selectorRead.selectedKeys().iterator();
        try {
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                SocketChannel channel = (SocketChannel) selectionKey.channel();
                readLen.set(channel.read(buffer));
            }
        } catch (IOException e) {
            throw new IllegalStateException("读取数据失败:", e);
        }
        if (readLen.get() <= 0) {
            //如果服务端断开链接，客户端则关闭
            if (readLen.get() == -1) {
                socketChannel.close();
            }
            throw new ReadFailException("读取数据失败，readLen=" + readLen.get());
        } else {
            retryTimes.set(0);
            return MessageParseUtils.parse(buffer);
        }
    }

    public IdDactylogramClient setMaxRetryTimes(int maxRetryTimes) {
        this.maxRetryTimes = maxRetryTimes;
        return this;
    }

    public IdDactylogramClient setSleepMsRetryBetween(int sleepMsRetryBetween) {
        this.sleepMsRetryBetween = sleepMsRetryBetween;
        return this;
    }

    public void runReloadAfter(Object obj) {
        try {
            //从新加载数据
            this.init();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
